package org.longteng.security.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Arrays;

/**
 * 国密算法SM4（分组对称密码算法，用于替代DES/AES等国际算法）
 * @author hymn.com
 * @date 2023/03/21 10:00:00
 */
@Slf4j
public class SM4UtilsBean {
    static {
        Security.addProvider(new BouncyCastleProvider());
    }
    // 默认密钥
    public static final String DEFAULT_KEY_HEX = Hex.toHexString("SM4#56789H123456".getBytes(StandardCharsets.UTF_8));

    private static final String ENCODING = "UTF-8";
    public static final String ALGORITHM_NAME = "SM4";
    // 128-32位16进制；256-64位16进制
    public static final int DEFAULT_KEY_SIZE = 128;

    private Key sm4Key;
    private Cipher encryptCipher;
    private Cipher decryptCipher;
    private boolean encryptInitialized = false;
    private boolean decryptInitialized = false;

    /**
     * 构建方法
     * @param algorithModel 算法名称ECB/CBC
     * @param key 密钥, 用generateKey生成
     */
    public SM4UtilsBean(SM4AlgModel algorithModel, byte[] key) throws Exception {
        this.encryptCipher = generateSM4Cipher(algorithModel, -1, key);
    }

    /**
     * 构建方法
     * @param algorithModel 算法名称ECB/CBC
     * @param hexKey 16进制密钥，可通过parseHexKey()生成
     */
    public SM4UtilsBean(SM4AlgModel algorithModel, String hexKey) throws Exception {
       // 16进制字符串-->byte[]
        byte[] keyData =  Hex.decodeStrict(hexKey) ; //ByteUtils.fromHexString(hexKey);
        this.encryptCipher = generateSM4Cipher(algorithModel, -1, keyData);
    }

    /**
     * 构建方法
     * @param algorithModel 算法名称ECB/CBC
     * @param hexKey 16进制密钥，可通过parseHexKey()生成
     */
    public SM4UtilsBean(SM4AlgModel algorithModel, String hexKey, int opmode) throws Exception {
        // 16进制字符串-->byte[]
        byte[] keyData =  Hex.decodeStrict(hexKey) ; //ByteUtils.fromHexString(hexKey);
        if(opmode == Cipher.ENCRYPT_MODE) {
            this.encryptCipher = generateSM4Cipher(algorithModel, opmode, keyData);
        } else if (opmode == Cipher.DECRYPT_MODE) {
            this.decryptCipher = generateSM4Cipher(algorithModel, opmode, keyData);
        }
    }

    /**
     * 生成ECB/CBC暗号
     * @explain ECB模式（电子密码本模式：Electronic codebook）,CBC 不容易主动攻击，安全性好，是SSL、IPSec的标准
     * @param algorithModel 算法名称ECB/CBC
     * @param mode 模式 加密Cipher.ENCRYPT_MODE，解密Cipher.DECRYPT_MODE
     * @param key  密钥
     * @return
     * @throws Exception
     */
    private Cipher generateSM4Cipher(SM4AlgModel algorithModel, int mode, byte[] key) throws Exception {
        Cipher cipher = Cipher.getInstance(algorithModel.getAlgName(), BouncyCastleProvider.PROVIDER_NAME);
        this.sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
        if (mode > -1) {  // -1时不初始化是加密还是解密码
            cipher.init(mode, sm4Key);
            if(mode == Cipher.ENCRYPT_MODE) {
                this.encryptInitialized = true;
            } else if (mode == Cipher.DECRYPT_MODE) {
                this.decryptInitialized = true;
            }
        }
        return cipher;
    }

    /**
     * 初始化 加密
     * @return
     * @throws InvalidKeyException
     */
    private boolean checkEncryptCipherInit() throws InvalidKeyException {
        if (! this.encryptInitialized) {
            if(this.encryptCipher == null) {
                try {
                    this.encryptCipher = Cipher.getInstance(this.decryptCipher.getAlgorithm(), BouncyCastleProvider.PROVIDER_NAME);
                } catch ( NoSuchPaddingException | NoSuchAlgorithmException | NoSuchProviderException e) {
                    log.error("创建SM4加密类错误.",e);
                    throw new InvalidKeyException("创建SM4加密类错误.",e);
                }
            }
            this.encryptCipher.init(Cipher.ENCRYPT_MODE, this.sm4Key);
            this.encryptInitialized = true;
        }
        return this.encryptInitialized;
    }


    /**
     * 初始化 解密
     * @return
     * @throws InvalidKeyException
     */
    private boolean checkDecryptCipherInit() throws InvalidKeyException {
        if (! this.decryptInitialized) {
            if(this.decryptCipher == null) { // 未创建时创建
                try {
                    this.decryptCipher = Cipher.getInstance(this.encryptCipher.getAlgorithm(), BouncyCastleProvider.PROVIDER_NAME);
                } catch ( NoSuchPaddingException | NoSuchAlgorithmException | NoSuchProviderException e) {
                    log.error("创建SM4解密类错误.",e);
                    throw new InvalidKeyException("创建SM4解密类错误.",e);
                }
            }
            this.decryptCipher.init(Cipher.DECRYPT_MODE, this.sm4Key);
            this.decryptInitialized = true;
        }
        return this.decryptInitialized;
    }

    /**
     * 生成密钥16进制密钥（忽略大小写）
     * @param key
     * @return 16进制密钥（忽略大小写），必须是16位
     */
    public static String parseHexKey(String key) {
        String hexKey = Hex.toHexString(StringUtils.left(StringUtils.rightPad(key==null?"myKey":key,16,'0'),16).getBytes(StandardCharsets.UTF_8));
        return hexKey;
    }

    /**
     * 自动生成密钥
     * @explain
     * @return
     * @throws NoSuchAlgorithmException
     * @throws NoSuchProviderException
     */
    public static byte[] generateKey() throws Exception {
        return generateKey(DEFAULT_KEY_SIZE);
    }
    /**
     * 系统产生秘钥
     * @explain 系统产生秘钥
     * @param keySize
     * @return
     * @throws Exception
     */
    public static byte[] generateKey(int keySize) throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(keySize, new SecureRandom());
        return kg.generateKey().getEncoded();
    }

    //加密******************************************

    /**
     *  sm4加密
     *  用默认密钥
     * @param paramStr 待加密字符串
     * @return 返回16进制的加密字符串
     * @throws Exception
     */
    public static String encryptBcb(String paramStr) throws Exception {
        return encryptBcb(DEFAULT_KEY_HEX, paramStr);
    }

    /**
     * sm4加密
     * @explain 加密模式：BCB 密文长度不固定，会随着被加密字符串长度的变化而变化
     * @param hexKey 16进制密钥
     * @param paramStr 待加密字符串
     * @return 返回16进制的加密字符串
     * @throws Exception
     */
    public static String encryptBcb(String hexKey, String paramStr) throws Exception {
        SM4UtilsBean sm4Utils = new SM4UtilsBean(SM4AlgModel.CBC, hexKey, Cipher.ENCRYPT_MODE);
        // 加密后的数组
        return sm4Utils.encryptStr( paramStr);
    }

    /**
     * sm4加密,标准字符(用默认密钥)
     * @param paramStr
     * @return
     * @throws Exception
     */
    public String encryptStr(String paramStr) throws Exception {
        // String-->byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 加密后的数组
        byte[] cipherArray = this.encrypt(srcData);
        // byte[]-->hexString
        // cipherText = ByteUtils.toHexString(cipherArray); //过期 用 new String(Hex.encode(resultHash))
        String cipherText = Hex.toHexString(cipherArray);
        return cipherText;
    }

    /**
     * 加密
     * @param data  要加密的数据
     * @return
     * @throws Exception
     */
    public byte[] encrypt(byte[] data) throws Exception {
        // Cipher cipher = generateSM4Cipher(SM4AlgModel.ECB, Cipher.ENCRYPT_MODE, key);//声称Ecb暗号,通过第二个参数判断加密还是解密
        this.checkEncryptCipherInit();
        return this.encryptCipher.doFinal(data);
    }

    //解密****************************************
    /**
     * sm4解密
     * @explain 解密模式：可采CBC
     * @param hexKey 16进制密钥
     * @param cipherText 16进制的加密字符串（忽略大小写）
     * @return 解密后的字符串
     * @throws Exception
     */
    public static String decryptCBC(String hexKey, String cipherText) throws Exception {
        // 用于接收解密后的字符串
        SM4UtilsBean sm4Utils = new SM4UtilsBean(SM4AlgModel.CBC, hexKey, Cipher.DECRYPT_MODE);
        // 解密
        return sm4Utils.decryptStr(cipherText);

    }

    /**
     * sm4解密,标准字符(用默认密钥)
     * @param cipherText
     * @return
     * @throws Exception
     */
    public String decryptStr(String cipherText) throws Exception {
        // hexString-->byte[]
        byte[] cipherData = Hex.decodeStrict(cipherText);      // ByteUtils.fromHexString(cipherText);
        // 解密
        byte[] srcData = this.decrypt(cipherData);
        // byte[]-->String
        String decryptStr = new String(srcData, ENCODING);
        return decryptStr;
    }

    /**
     * 解密
     * @explain
     * @param cipherText
     * @return
     * @throws Exception
     */
    public byte[] decrypt(byte[] cipherText) throws Exception {
        // Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);//生成Ecb暗号,通过第二个参数判断加密还是解密
        this.checkDecryptCipherInit();
        return this.decryptCipher.doFinal(cipherText);
    }

    /**
     * 校验加密前后的字符串是否为同一数据
     * @explain
     * @param cipherText 16进制加密后的字符串
     * @param paramStr 加密前的字符串
     * @return 是否为同一数据
     * @throws Exception
     */
    public boolean verify(String cipherText, String paramStr) throws Exception {
        // 用于接收校验结果
        boolean flag = false;

        // 将16进制字符串转换成数组
        byte[] cipherData = Hex.decodeStrict(cipherText);   // ByteUtils.fromHexString(cipherText);
        // 解密
        byte[] decryptData = this.decrypt(cipherData);
        // 将原字符串转换成byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 判断2个数组是否一致
        flag = Arrays.equals(decryptData, srcData);
        return flag;
    }

    /**
     * 校验加密前后的字符串是否为同一数据
     * @explain
     * @param hexKey 16进制密钥
     * @param cipherText 16进制加密后的字符串
     * @param paramStr 加密前的字符串
     * @return 是否为同一数据
     * @throws Exception
     */
    public static boolean verifyCbc(String hexKey, String cipherText, String paramStr) throws Exception {
        // 用于接收校验结果
        boolean flag = false;
        // 将16进制字符串转换成数组
        byte[] cipherData = Hex.decodeStrict(cipherText);   // ByteUtils.fromHexString(cipherText);
        SM4UtilsBean sm4Utils = new SM4UtilsBean(SM4AlgModel.CBC, hexKey, Cipher.DECRYPT_MODE);
        // 解密
        byte[] decryptData = sm4Utils.decrypt(cipherData);
        // 将原字符串转换成byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 判断2个数组是否一致
        flag = Arrays.equals(decryptData, srcData);
        return flag;
    }

    /**
     * SM4用的两种加密模式
     */
    public enum SM4AlgModel {
        ECB("SM4/ECB/PKCS5Padding", 1), CBC("SM4/CBC/PKCS5Padding", 2);
        // 加密算法/分组加密模式/分组填充方式
        // PKCS5Padding-以8个字节为一组进行分组加密
        // 定义分组加密模式使用：PKCS5Padding; 如国际AES在：Cipher.getInstance("AES/ECB/PKCS7Padding");
        // ECB（电子密本方式）其实非常简单，就是将数据按照8个字节一段进行DES加密或解密得到一段段的8个字节的密文或者明文，最后一段不足8个字节（一般补0或者F）
        // static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";
        // CBC 不容易主动攻击，安全性好于ECB，是SSL、IPSec的标准；
        // static final String ALGORITHM_NAME_CBC_PADDING = "SM4/CBC/PKCS5Padding";
        private String algName;
        private int index;
        // 构造方法
        private SM4AlgModel(String algName, int index) {
            this.algName = algName;
            this.index = index;
        }

        public String getAlgName() {
            return algName;
        }
    }
}
